/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.ide.projectPane.logicalview; import com.intellij.ide.projectView.ProjectView; import com.intellij.ide.projectView.impl.AbstractProjectViewPane; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionPlaces; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.DumbService.DumbModeListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.util.ArrayUtil; import com.intellij.util.messages.MessageBusConnection; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.ide.projectPane.BaseLogicalViewProjectPane; import jetbrains.mps.ide.projectPane.ProjectPane; import jetbrains.mps.ide.projectPane.ProjectPaneActionGroups; import jetbrains.mps.ide.projectPane.ProjectPaneDnDListener; import jetbrains.mps.ide.projectPane.logicalview.highlighting.ProjectPaneTreeHighlighter; import jetbrains.mps.ide.ui.smodel.ConceptTreeNode; import jetbrains.mps.ide.ui.smodel.PropertiesTreeNode; import jetbrains.mps.ide.ui.smodel.ReferencesTreeNode; import jetbrains.mps.ide.ui.tree.MPSTreeNode; import jetbrains.mps.ide.ui.tree.module.ProjectModuleTreeNode; import jetbrains.mps.ide.ui.tree.module.SModelsSubtree; import jetbrains.mps.ide.ui.tree.smodel.NodeTargetProvider; import jetbrains.mps.ide.ui.tree.smodel.PackageNode; import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode; import jetbrains.mps.ide.ui.tree.smodel.SNodeTreeNode.NodeChildrenProvider; import jetbrains.mps.openapi.navigation.EditorNavigator; import jetbrains.mps.project.DevKit; import jetbrains.mps.project.Solution; import jetbrains.mps.smodel.Generator; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.SModelStereotype; import jetbrains.mps.smodel.SNodeUtil; import jetbrains.mps.util.Computable; import jetbrains.mps.util.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeAccessUtil; import org.jetbrains.mps.openapi.model.SNodeReference; import javax.swing.tree.TreePath; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceAdapter; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DropTarget; import java.awt.dnd.InvalidDnDOperationException; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; /** * GUESS: while {@link ProjectTree} is deemed for embedded UI components, e.g. in a dialog, * this class is intended solely for ProjectPane, thus supports DnD, highlighting (although this might * need move to ProjectPane, as it's project stuff and needs Idea's project Message bus), integration with * editor (activation, auto-select/expand), etc. */ public class ProjectPaneTree extends ProjectTree implements NodeChildrenProvider, ProjectModuleTreeNode.ModuleNodeChildrenProvider { private ProjectPane myProjectPane; private KeyAdapter myKeyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getModifiers() != 0 || e.getKeyCode() != KeyEvent.VK_ENTER) { return; } TreePath selPath = getSelectionPath(); if (selPath != null && selPath.getLastPathComponent() instanceof MPSTreeNode) { // reuse method for double click doubleClick((MPSTreeNode) selPath.getLastPathComponent()); e.consume(); } } }; private final ProjectPaneTreeHighlighter myHighlighter; private final TreeStructureUpdate myStructureUpdate; public ProjectPaneTree(ProjectPane projectPane, Project project) { super(ProjectHelper.fromIdeaProject(project)); myProjectPane = projectPane; myHighlighter = new ProjectPaneTreeHighlighter(this, ProjectHelper.fromIdeaProject(project)); myHighlighter.init(); myStructureUpdate = new TreeStructureUpdate(this); myStructureUpdate.init(); //enter can't be listened using keyboard actions because in this case tree's UI receives it first and just expands a node addKeyListener(myKeyListener); //drag support is alive while the tree is alive DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, new MyDragGestureListener()); new DropTarget(this, new ProjectPaneDnDListener(this, new MyTransferable(null).getTransferDataFlavors()[0])); MessageBusConnection connection = project.getMessageBus().connect(); Disposer.register(this, connection); connection.subscribe(DumbService.DUMB_MODE, new DumbModeListener() { @Override public void enteredDumbMode() { // there used to be update both on enter and exit of the dumb mode, however, I don't see a reason to // do it twice. Moreover, there's guard condition in TreeUpdateVisitor that waits for dumb mode to complete. } @Override public void exitDumbMode() { myHighlighter.dumbUpdate(); } }); } @Override public void runRebuildAction(Runnable rebuildAction, boolean saveExpansion) { super.runRebuildAction(rebuildAction, saveExpansion); // gen status is tracked on model level. If there are no model nodes shown yet, generation status for module and // namespace nodes shall be updated. Other alternative is to do it in ModuleNodeListener#attach() or in // ProjectPaneTreeHighlighter#moduleNodeAdded. MNL at the moment doesn't track gen status notifications, so it's odd to put // update there. PPTH#moduleNodeAdded() is decent alternative and perhaps the right thing to do, however, I decided // to give it a 'single-shot' approach, to re-highlight a tree once rebuild is over, which seems reasonable. myHighlighter.dumbUpdate(); } @Override public void dispose() { myStructureUpdate.dispose(); myHighlighter.dispose(); removeKeyListener(myKeyListener); super.dispose(); } @Override public Comparator<Object> getChildrenComparator() { return myProjectPane.getTreeChildrenComparator(); } @Override protected void doubleClick(@NotNull MPSTreeNode nodeToClick) { if (nodeToClick instanceof NodeTargetProvider) { final SNodeReference navigationTarget = ((NodeTargetProvider) nodeToClick).getNavigationTarget(); if (navigationTarget != null) { new EditorNavigator(getProject()).shallFocus(true).selectIfChild().open(navigationTarget); return; } // fall-through } super.doubleClick(nodeToClick); } @Override protected void autoscroll(@NotNull MPSTreeNode nodeToClick) { if (nodeToClick instanceof NodeTargetProvider) { final SNodeReference navigationTarget = ((NodeTargetProvider) nodeToClick).getNavigationTarget(); if (navigationTarget != null) { new EditorNavigator(getProject()).shallFocus(false).selectIfChild().open(navigationTarget); return; } // fall-through } super.autoscroll(nodeToClick); } @Override public boolean isAutoOpen() { return myProjectPane.getProjectView().isAutoscrollToSource(myProjectPane.getId()); } @Override protected String getPopupMenuPlace() { return ActionPlaces.PROJECT_VIEW_POPUP; } @Override protected ActionGroup createPopupActionGroup(final MPSTreeNode node) { return new ModelAccessHelper(getProject().getModelAccess()).runReadAction(new Computable<ActionGroup>() { @Override public ActionGroup compute() { return ProjectPaneActionGroups.getActionGroup(node); } }); } @Override public void populate(SNodeTreeNode treeNode) { if (myProjectPane.showNodeStructure()) { SNode n = treeNode.getSNode(); if (n == null || n.getModel() == null) { return; } treeNode.add(new ConceptTreeNode(n)); treeNode.add(new PropertiesTreeNode(n)); treeNode.add(new ReferencesTreeNode(n)); } } @Override public boolean populate(MPSTreeNode treeNode, Language language) { return false; } @Override public boolean populate(MPSTreeNode treeNode, Solution solution) { return false; } @Override public boolean populate(MPSTreeNode treeNode, Generator generator) { if (myProjectPane.isDescriptorModelInGeneratorVisible()) { return false; } Predicate<SModel> isDescriptorModel = SModelStereotype::isDescriptorModel; new SModelsSubtree(treeNode).create(generator.getModels().stream().filter(isDescriptorModel.negate()).collect(Collectors.toList())); return true; } @Override public boolean populate(MPSTreeNode treeNode, DevKit devkit) { return false; } private class MyTransferable implements Transferable { private final String mySupportedFlavor = "MPSNodeToMoveFlavor"; private Object myObject; public MyTransferable(Object o) { myObject = o; } @Override public DataFlavor[] getTransferDataFlavors() { Class aClass = MyTransferable.class; DataFlavor dataFlavor = null; try { dataFlavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=" + aClass.getName(), mySupportedFlavor, aClass.getClassLoader()); } catch (ClassNotFoundException ignored) { } return new DataFlavor[]{dataFlavor}; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { DataFlavor[] flavors = getTransferDataFlavors(); return ArrayUtil.find(flavors, flavor) != -1; } @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { return myObject; } } private class MyDragSourceListener extends DragSourceAdapter { @Override public void dragEnter(DragSourceDragEvent dsde) { dsde.getDragSourceContext().setCursor(null); } @Override public void dropActionChanged(DragSourceDragEvent dsde) { dsde.getDragSourceContext().setCursor(null); } } private class MyDragGestureListener implements DragGestureListener { @Override public void dragGestureRecognized(final DragGestureEvent dge) { if ((dge.getDragAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) { return; } ProjectView projectView = ProjectView.getInstance(myProjectPane.getProject()); if (projectView == null) { return; } final AbstractProjectViewPane currentPane = projectView.getCurrentProjectViewPane(); if (!(currentPane instanceof BaseLogicalViewProjectPane)) { return; } final List<Pair<SNodeReference, String>> result = new ArrayList<>(); getProject().getModelAccess().runReadAction(new Runnable() { @Override public void run() { for (SNode node : myProjectPane.getSelectedSNodes()) { result.add(new Pair<>(new jetbrains.mps.smodel.SNodePointer(node), "")); } SModel contextDescriptor = myProjectPane.getContextModel(); if (contextDescriptor != null) { for (PackageNode treeNode : myProjectPane.getSelectedTreeNodes(PackageNode.class)) { String searchedPack = treeNode.getFullPackage(); if (treeNode.getChildCount() == 0 || searchedPack == null) { continue; } for (final SNode node : contextDescriptor.getRootNodes()) { String nodePack = SNodeAccessUtil.getProperty(node, SNodeUtil.property_BaseConcept_virtualPackage); if (nodePack == null) { continue; } if (!nodePack.startsWith(searchedPack)) { continue; } StringBuilder basePack = new StringBuilder(); String firstPart = treeNode.getPackage(); String secondPart = ""; if (nodePack.startsWith(searchedPack + ".")) { secondPart = nodePack.replaceFirst(searchedPack + ".", ""); } basePack.append(firstPart); if (!firstPart.isEmpty() && !secondPart.isEmpty()) { basePack.append("."); } basePack.append(secondPart); result.add(new Pair<>(new jetbrains.mps.smodel.SNodePointer(node), basePack.toString())); } } } } }); if (result.isEmpty()) { return; } try { dge.startDrag(DragSource.DefaultMoveNoDrop, new MyTransferable(result), new MyDragSourceListener()); } catch (InvalidDnDOperationException ignored) { } } } }